// 
// Copyright 2010 Classpath Pty Ltd <opensource@classpath.com.au>
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 

**
** This class provides a generic implementation of the `Adapter` class.
**
class GenericAdapter : Adapter {

    **
    ** Contains a mapping of [ColumnTypes]`ColumnType` to their SQL representations.
    ** Subclasses should set their own values to appropriate substitutes for variations of this, or if they handle custom types include them.
    **
    [Type:Str] sqlTypes := [BlobType#: "BLOB",
                            CharType#: "CHAR",
                            DateType#: "DATE",
                            DecimalType#: "DECIMAL",
                            DoubleType#: "DOUBLE",
                            FloatType#: "FLOAT",
                            IntegerType#: "INTEGER",
                            SerialType#: "SERIAL",
                            TextType#: "TEXT",
                            TimestampType#: "TIMESTAMP",
                            TimeType#: "TIME",
                            VarcharType#: "VARCHAR"]

    **
    ** Generates 'CREATE TABLE' SQL for the supplied `CreatedTable`.
    **
    override Str createTableSql(CreatedTable table) {
        Pod.of(this).log.debug("${typeof}.createTableSql(${table}) - start ")
        buf := StrBuf()
        buf.join("CREATE TABLE ${table.name}")
        columns := table.modifications.findAll { it is CreatedColumn } .map { createColumnSql(it) }
        options := table.modifications.findAll { it is TableOption } .map { tableOptionSql(it) }
        clauses := [,].addAll(columns).addAll(options)
        if (!clauses.isEmpty) {
            buf.join("(")
            buf.join(clauses.join(",\n"), "\n")
            buf.join(")", "\n")
        }
        Pod.of(this).log.debug("${typeof}.createTableSql(${table}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates 'ALTER TABLE' SQL for the supplied `AlteredTable`.
    **
    override Str alterTableSql(AlteredTable table) {
        Pod.of(this).log.debug("${typeof}.alterTableSql(${table}) - start ")
        buf := StrBuf()
        buf.join("ALTER TABLE ${table.name}")
        createdColumns := table.modifications.findAll { it is CreatedColumn } .map { createColumnSql(it, true) }
        droppedColumns := table.modifications.findAll { it is DroppedColumn } .map { dropColumnSql(it) }
        createdOptions := table.modifications.findAll { it is TableOption } .map { tableOptionSql(it, true) }
        droppedOptions := table.modifications.findAll { it is DroppedTableOption } .map { dropTableOptionSql(it) }
        clauses := [,].addAll(createdColumns).addAll(droppedColumns).addAll(createdOptions).addAll(droppedOptions)
        if (!clauses.isEmpty) {
            buf.join(clauses.join(",\n"), "\n")
        }
        Pod.of(this).log.debug("${typeof}.alterTableSql(${table}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates 'DROP TABLE' SQL for the supplied `DroppedTable`.
    **
    override Str dropTableSql(DroppedTable table) {
        Pod.of(this).log.debug("${typeof}.dropTableSql(${table}) - start ")
        buf := StrBuf()
        buf.join("DROP TABLE ${table.name}")
        Pod.of(this).log.debug("${typeof}.dropTableSql(${table}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates SQL for the supplied `ExecutedSql` (simply returns sql.sql).
    **
    override Str executeSql(ExecutedSql sql) {
        Pod.of(this).log.debug("${typeof}.executeSql(${sql}) - start ")
        buf := StrBuf()
        buf.join(sql.sql)
        Pod.of(this).log.debug("${typeof}.executeSql(${sql}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates SQL for a prepared statement with paramters '@from' and '@to' for logging an update to the version history table created in `MigrationZero`.
    **
    override Str logMigrationSql() {
        Pod.of(this).log.debug("${typeof}.logMigrationSql - start ")
        buf := StrBuf()
        buf.add("INSERT INTO ${MigrationZero.tableName} (")
        buf.add("${MigrationZero.fromVersionColumnName}, ${MigrationZero.toVersionColumnName}")
        buf.add(") VALUES (")
        buf.add("@from, @to")
        buf.add(")")
        Pod.of(this).log.debug("${typeof}.logMigrationSql - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates SQL to select the latest version from the version history table created in `MigrationZero`.
    **
    override Str dbVersionSql() {
        Pod.of(this).log.debug("${typeof}.dbVersionSql - start ")
        buf := StrBuf()
        buf.add("SELECT ${MigrationZero.toVersionColumnName}")
        buf.join("FROM ${MigrationZero.tableName}")
        buf.join("WHERE ${MigrationZero.idColumnName} = (")
        buf.add("SELECT MAX(${MigrationZero.idColumnName})")
        buf.join("FROM ${MigrationZero.tableName}")
        buf.add(")")
        Pod.of(this).log.debug("${typeof}.dbVersionSql - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates the SQL for a column's complete definition for use in a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str createColumnSql(CreatedColumn column, Bool includeAdd := false) {
        Pod.of(this).log.debug("${typeof}.createColumnSql(${column}, ${includeAdd}) - start")
        buf := StrBuf()
        if (includeAdd) {
            buf.join("ADD COLUMN")
        }
        buf.join(column.name)
        buf.join(columnDefinition(column.type))
        column.modifications.find { it is AutoIncrementColumnOption } ?.with {
            sql := autoIncrementColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is CheckColumnOption } ?.with {
            sql := checkColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is CommentColumnOption } ?.with {
            sql := commentColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is DefaultValueColumnOption } ?.with {
            sql := defaultValueColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is NotNullColumnOption } ?.with {
            sql := notNullColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is PrimaryKeyColumnOption } ?.with {
            sql := primaryKeyColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is ReferencesColumnOption } ?.with {
            sql := referencesColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        column.modifications.find { it is UniqueColumnOption } ?.with {
            sql := uniqueColumnOptionSql(it)
            if (!sql.isEmpty) {
                buf.join(sql)
            }
        }
        Pod.of(this).log.debug("${typeof}.createColumnSql(${column}, ${includeAdd}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'AUTO_INCREMENT' option based on the supplied column option
    **
    virtual Str autoIncrementColumnOptionSql(AutoIncrementColumnOption option) {
        Pod.of(this).log.debug("${typeof}.autoIncrementColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("AUTO_INCREMENT")
        Pod.of(this).log.debug("${typeof}.autoIncrementColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'CHECK' option based on the supplied column option
    **
    virtual Str checkColumnOptionSql(CheckColumnOption option) {
        Pod.of(this).log.debug("${typeof}.checkColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("CHECK (${option.check})")
        Pod.of(this).log.debug("${typeof}.checkColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'COMMENT' option based on the supplied column option
    **
    virtual Str commentColumnOptionSql(CommentColumnOption option) {
        Pod.of(this).log.debug("${typeof}.commentColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("COMMENT '${option.comment}'")
        Pod.of(this).log.debug("${typeof}.commentColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'DEFAULT' option based on the supplied column option
    **
    virtual Str defaultValueColumnOptionSql(DefaultValueColumnOption option) {
        Pod.of(this).log.debug("${typeof}.defaultValueColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("DEFAULT ${option.defaultValue}")
        Pod.of(this).log.debug("${typeof}.defaultValueColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'NOT NULL' option based on the supplied column option
    **
    virtual Str notNullColumnOptionSql(NotNullColumnOption option) {
        Pod.of(this).log.debug("${typeof}.notNullColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("NOT NULL")
        Pod.of(this).log.debug("${typeof}.notNullColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'PRIMARY KEY' option based on the supplied column option
    **
    virtual Str primaryKeyColumnOptionSql(PrimaryKeyColumnOption option) {
        Pod.of(this).log.debug("${typeof}.primaryKeyColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("PRIMARY KEY")
        Pod.of(this).log.debug("${typeof}.primaryKeyColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'REFERENCES' option based on the supplied column option
    **
    virtual Str referencesColumnOptionSql(ReferencesColumnOption option) {
        Pod.of(this).log.debug("${typeof}.referencesColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("REFERENCES ${option.reference.table} (${option.reference.columns[0]})")
        Pod.of(this).log.debug("${typeof}.referencesColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates an 'UNIQUE' option based on the supplied column option
    **
    virtual Str uniqueColumnOptionSql(UniqueColumnOption option) {
        Pod.of(this).log.debug("${typeof}.uniqueColumnOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("UNIQUE")
        Pod.of(this).log.debug("${typeof}.uniqueColumnOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates the SQL for a 'DROP COLUMN' definition for use in an 'ALTER TABLE' statement.
    **
    virtual Str dropColumnSql(DroppedColumn column) {
        Pod.of(this).log.debug("${typeof}.dropColumnSql(${column}) - start")
        buf := StrBuf()
        buf.join("DROP COLUMN ${column.name}")
        Pod.of(this).log.debug("${typeof}.dropColumnSql(${column}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates the SQL for a table's options for use in a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str tableOptionSql(TableOption option, Bool includeAdd := false) {
        Pod.of(this).log.debug("${typeof}.tableOptionSql(${option}) - start")
        buf := StrBuf()
        if (includeAdd) {
            buf.add("ADD")
        }
        switch (option.typeof) {
            case CheckTableOption#:
                buf.join(checkTableOptionSql(option))
            case ForeignKeyTableOption#:
                buf.join(foreignKeyTableOptionSql(option))
            case IndexTableOption#:
                buf.join(indexTableOptionSql(option))
            case PrimaryKeyTableOption#:
                buf.join(primaryKeyTableOptionSql(option))
            case UniqueTableOption#:
                buf.join(uniqueTableOptionSql(option))
            default:
                Pod.of(this).log.err("${typeof} doesn't know how to create a table option for ${option.typeof}")
                throw Err("${typeof} doesn't know how to create a table option for ${option.typeof}")
        }
        
        Pod.of(this).log.debug("${typeof}.tableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'CHECK' constraint for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str checkTableOptionSql(CheckTableOption option) {
        Pod.of(this).log.debug("${typeof}.checkTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("CONSTRAINT ${option.name} CHECK (${option.check})")
        Pod.of(this).log.debug("${typeof}.checkTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'FOREIGN KEY' constraint for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str foreignKeyTableOptionSql(ForeignKeyTableOption option) {
        Pod.of(this).log.debug("${typeof}.foreignKeyTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("CONSTRAINT ${option.name} FOREIGN KEY (")
        buf.add(option.columns.join(", "))
        buf.add(") REFERENCES ${option.reference.table} (")
        buf.add(option.reference.columns.join(", "))
        buf.add(")")
        Pod.of(this).log.debug("${typeof}.foreignKeyTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'INDEX' statement for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str indexTableOptionSql(IndexTableOption option) {
        Pod.of(this).log.debug("${typeof}.indexTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("INDEX ${option.name} (")
        buf.add(option.columns.join(", "))
        buf.add(")")
        Pod.of(this).log.debug("${typeof}.indexTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'PRIMARY KEY' constraint for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str primaryKeyTableOptionSql(PrimaryKeyTableOption option) {
        Pod.of(this).log.debug("${typeof}.primaryKeyTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("CONSTRAINT ${option.name} PRIMARY KEY (")
        buf.add(option.columns.join(", "))
        buf.add(")")
        Pod.of(this).log.debug("${typeof}.primaryKeyTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'UNIQUE' constraint for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str uniqueTableOptionSql(UniqueTableOption option) {
        Pod.of(this).log.debug("${typeof}.uniqueTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("CONSTRAINT ${option.name} UNIQUE (")
        buf.add(option.columns.join(", "))
        buf.add(")")
        Pod.of(this).log.debug("${typeof}.uniqueTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates the SQL for dropping a table's options for a 'ALTER TABLE' statement.
    **
    virtual Str dropTableOptionSql(DroppedTableOption option) {
        Pod.of(this).log.debug("${typeof}.dropTableOption(${option}) - start")
        buf := StrBuf()
        switch (option.typeof) {
            case DroppedForeignKeyTableOption#:
                buf.join(dropForeignKeyTableOptionSql(option))
            case DroppedIndexTableOption#:
                buf.join(dropIndexTableOptionSql(option))
            case DroppedPrimaryKeyTableOption#:
                buf.join(dropPrimaryKeyTableOptionSql(option))
            default:
                Pod.of(this).log.err("${typeof} doesn't know how to drop a table option for ${option.typeof}")
                throw Err("${typeof} doesn't know how to drop a table option for ${option.typeof}")
        }
        
        Pod.of(this).log.debug("${typeof}.dropTableOption(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'DROP FOREIGN KEY' statement for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str dropForeignKeyTableOptionSql(DroppedForeignKeyTableOption option) {
        Pod.of(this).log.debug("${typeof}.dropForeignKeyTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("DROP FOREIGN KEY ${option.name}")
        Pod.of(this).log.debug("${typeof}.dropForeignKeyTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'DROP INDEX' statement for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str dropIndexTableOptionSql(DroppedIndexTableOption option) {
        Pod.of(this).log.debug("${typeof}.dropIndexTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("DROP INDEX ${option.name}")
        Pod.of(this).log.debug("${typeof}.dropIndexTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates a 'DROP PRIMARY KEY' statement for inclusion as part of a 'CREATE TABLE' or 'ALTER TABLE' statement.
    **
    virtual Str dropPrimaryKeyTableOptionSql(DroppedPrimaryKeyTableOption option) {
        Pod.of(this).log.debug("${typeof}.dropPrimaryKeyTableOptionSql(${option}) - start")
        buf := StrBuf()
        buf.join("DROP PRIMARY KEY ${option.name}")
        Pod.of(this).log.debug("${typeof}.dropPrimaryKeyTableOptionSql(${option}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

    **
    ** Generates the SQL for a column's type definition.
    **
    virtual Str columnDefinition(ColumnType type) {
        Pod.of(this).log.debug("${typeof}.columnDefinition(${type}) - start")
        buf := StrBuf()
        sqlType := sqlTypes[type.typeof]
        if (sqlType == null) {
            Pod.of(this).log.err("${typeof} doesn't know the SQL type for ${type.typeof} - did you store it in sqlTypes?")
            throw Err("${typeof} doesn't know the SQL type for ${type.typeof} - did you store it in sqlTypes?")
        }
        buf.join(sqlType)
        if (type is HasScale) {
            scale := type as HasScale
            if (scale.precision > 0 && scale.scale > 0) {
                buf.add("(${scale.precision}, ${scale.scale})")
            }
            else if (scale.precision > 0) {
                buf.add("(${scale.precision})")
            }
        }
        else if (type is HasPrecision) {
            precision := type as HasPrecision
            if (precision.precision > 0) {
                buf.add("(${precision.precision})")
            }
        }
        if (type is HasLength) {
            length := type as HasLength
            if (length.length > 0) {
                buf.add("(${length.length})")
            }
        }
        Pod.of(this).log.debug("${typeof}.columnDefinition(${type}) - end - returning ${buf.toStr}")
        return buf.toStr
    }

}
